﻿using System;
using System.Linq.Expressions;
using Linq = System.Linq.Expressions;
using System.Reflection;
using System.Linq;

namespace LinqToAmazonSL
{
    //This class is used to create an AmazonQueryCriteria from an expression tree.
    public class AmazonExpressionVisitor
    {
        AmazonQueryCriteria Criteria;

        public AmazonExpressionVisitor(AmazonQueryCriteria criteriaType)
        {
            Criteria = criteriaType;
        }

        //Starts the parsing of an expression
        public AmazonQueryCriteria ProcessExpression(Linq.Expression expression)
        {
            VisitExpression(expression);
            return Criteria;
        }


        private void VisitExpression(Linq.Expression expression)
        {
            //Identifies the node type and calls the next method to continue with the parsing
            BinaryExpression bExp = null;
            if ((expression.NodeType.CompareTo(ExpressionType.AndAlso) == 0)
                || (expression.NodeType.CompareTo(ExpressionType.Equal) == 0)
                || (expression.NodeType.CompareTo(ExpressionType.LessThanOrEqual) == 0))
            {
                bExp = (BinaryExpression)expression;
            }
            switch (expression.NodeType)
            {
                case ExpressionType.AndAlso:
                    VisitAndAlso(bExp);
                    break;
                case ExpressionType.Equal:
                    VisitEqual(bExp);
                    break;
                case ExpressionType.LessThanOrEqual:
                    VisitLessThanOrEqual(bExp);
                    break;
                case ExpressionType.Call:
                    VisitMethodCall((MethodCallExpression)expression);
                    break;
                case ExpressionType.Lambda:
                    VisitExpression(((LambdaExpression)expression).Body);
                    break;
            }
        }

        //Parse a binary expression
        private void VisitAndAlso(BinaryExpression andAlso)
        {
            VisitExpression(andAlso.Left);
            VisitExpression(andAlso.Right);
        }

        //Parse an equal expression
        private void VisitEqual(BinaryExpression expression)
        {
            //Identifies the left side of the expression
            if (expression.Left.NodeType == ExpressionType.MemberAccess)
            {
                //Sets the new parameter value as the name of the left side of the equal expression and indicates its an equal
                string parameterKey = ((MemberExpression)expression.Left).Member.Name;
                QueryValue value = new QueryValue() { Operator = OperatorQ.Equal };

                //Identifies the right side of the equal expression
                if (expression.Right.NodeType == ExpressionType.Constant)
                {
                    //The right side is a constant so the new parameter value is allocated directly
                    value.Value = (String)((ConstantExpression)expression.Right).Value;
                }
                else if (expression.Right.NodeType == ExpressionType.MemberAccess)
                {
                    //The right side is a member access so the new parameter value is allocated by retrieving the value of this memmber
                    value.Value = (String)GetMemberValue((MemberExpression)expression.Right);
                }
                else if (expression.Right is MethodCallExpression)
                {
                    //The right side is a nested query so it execute the nested query assuming the result will be a string
                    //and allocate it. 
                    string result = (string)System.Linq.Expressions.Expression.Lambda<Func<object>>(expression.Right).Compile()();
                    value.Value = result;
                }
                else
                    throw new NotSupportedException("Expression type not supported " + expression.Right.NodeType.ToString());

                if (!Criteria.Parameters.ContainsKey(parameterKey))
                    //if the parameter wasn't allready added, it is added to the criteria.
                    Criteria.Parameters.Add(parameterKey, value);
            }
        }

        //Parse a less or equal expression
        private void VisitLessThanOrEqual(BinaryExpression expression)
        {
            //Sets the new parameter value as the name of the left side of the expression and indicates its a less or equal
            string parameterKey = ((MemberExpression)expression.Left).Member.Name;
            QueryValue value = new QueryValue() { Operator = OperatorQ.LessThanOrEqual };

            // Identifies the right side of the expression
            // and allocate the value of the parameter
            if ((expression.Left.NodeType == ExpressionType.MemberAccess))
            {
                if (expression.Right.NodeType == ExpressionType.Constant)
                    value.Value = (string)((ConstantExpression)expression.Right).Value;
                else if (expression.Right.NodeType == ExpressionType.MemberAccess)
                    value.Value = (string)GetMemberValue((MemberExpression)expression.Right);
                else if (expression.Right is MethodCallExpression)
                {
                    string result = (string)System.Linq.Expressions.Expression.Lambda<Func<object>>(expression.Right).Compile()();
                    value.Value = result;
                }
                else
                    throw new NotSupportedException("Expression type not supported " + expression.Right.NodeType.ToString());
            }

            if (!Criteria.Parameters.ContainsKey(parameterKey))
                //if the parameter wasn't allready added, it is added to the criteria.
                Criteria.Parameters.Add(parameterKey, value);
        }

        //Used to parse the operations, if the operations are not provider directly by Amazon, 
        //they are stored in the operation stack for later processing. Otherwise they are set as parameters
        //in the Criteria.
            
        private void VisitMethodCall(MethodCallExpression expression)
        {   
            string MethodName = expression.Method.Name;
            if (expression.Method.DeclaringType == typeof(Queryable))
            {
                Operations oper = null;
                //Identifies the opperation
                switch (MethodName)
                {
                    case "Select":
                        VisitExpression(expression.Arguments[0]);
                        //Sets the selector for doing the proyection to the results
                        Criteria.Selector = ((LambdaExpression)((UnaryExpression)(expression.Arguments[1])).Operand);
                        break;
                    case "Where":
                        //Keeps parsing the inner expression of the where
                        VisitExpression(((UnaryExpression)expression.Arguments[1]).Operand);
                        break;
                    //Operations not directly provided by Amazon
                    case "Count":
                        oper = new Operations(Oper.Count, "");
                        Criteria.OperationsStack.Push(oper);
                        VisitExpression(expression.Arguments[0]);
                        break;
                    case "Last":
                        oper = new Operations(Oper.Last, "");
                        Criteria.OperationsStack.Push(oper);
                        VisitExpression(expression.Arguments[0]);
                        break;
                    case "LastOrDefault":
                        oper = new Operations(Oper.LastOrDefault, "");
                        Criteria.OperationsStack.Push(oper);
                        VisitExpression(expression.Arguments[0]);
                        break;
                    case "ElementAt":
                        oper = new Operations(Oper.ElementAt, expression.Arguments[1].ToString());
                        Criteria.OperationsStack.Push(oper);
                        VisitExpression(expression.Arguments[0]);
                        break;
                    case "ElementAtOrDefault":
                        oper = new Operations(Oper.ElementAtOrDefault, expression.Arguments[1].ToString());
                        Criteria.OperationsStack.Push(oper);
                        VisitExpression(expression.Arguments[0]);
                        break;
                    case "Any":
                        oper = new Operations(Oper.Any, "");
                        Criteria.OperationsStack.Push(oper);
                        VisitExpression(expression.Arguments[0]);
                        break;
                    case "First":
                        oper = new Operations(Oper.First, "");
                        Criteria.OperationsStack.Push(oper);
                        VisitExpression(expression.Arguments[0]);
                        break;
                    case "FirstOrDefault":
                        oper = new Operations(Oper.FirstOrDefault, "");
                        Criteria.OperationsStack.Push(oper);
                        VisitExpression(expression.Arguments[0]);
                        break;
                    case "Take":
                        oper = new Operations(Oper.Take, expression.Arguments[1].ToString());
                        Criteria.OperationsStack.Push(oper);
                        VisitExpression(expression.Arguments[0]);
                        break;
                    case "Skip":
                        oper = new Operations(Oper.Skip, expression.Arguments[1].ToString());
                        Criteria.OperationsStack.Push(oper);
                        VisitExpression(expression.Arguments[0]);
                        break;
                    //Operations provided by Amazon
                    case "OrderBy":
                    case "OrderByDescending":
                        QueryValue value = new QueryValue();
                        try
                        {
                            value.Value = ((MemberExpression)(((LambdaExpression)((UnaryExpression)expression.Arguments[1]).Operand).Body)).Member.Name;
                        }
                        catch (Exception)
                        {
                            throw new NotSupportedException("Method not supported: " + MethodName + ", order by parameter missing");
                        }
                        if (!Criteria.Parameters.ContainsKey(MethodName))
                            Criteria.Parameters.Add(MethodName, value);
                        VisitExpression(expression.Arguments[0]);
                        break;
                    default:
                        throw new NotSupportedException("Method not supported: " + expression.Method.Name);
                }
            }
        }

        #region Helpers

        //Retrieves the value of a member expression
        private Object GetMemberValue(MemberExpression memberExpression)
        {
            MemberInfo memberInfo;
            Object obj;

            if (memberExpression == null)
                throw new ArgumentNullException("memberExpression");

            // Get object
            if (memberExpression.Expression is ConstantExpression)
                obj = ((ConstantExpression)memberExpression.Expression).Value;
            else if (memberExpression.Expression is MemberExpression)
                obj = GetMemberValue((MemberExpression)memberExpression.Expression);
            else
                throw new NotSupportedException("Expression type not supported: " + memberExpression.Expression.GetType().FullName);

            // Get value
            memberInfo = memberExpression.Member;
            if (memberInfo is PropertyInfo)
            {
                PropertyInfo property = (PropertyInfo)memberInfo;
                return property.GetValue(obj, null);
            }
            else if (memberInfo is FieldInfo)
            {
                object value = System.Linq.Expressions.Expression.Lambda<Func<object>>(memberExpression).Compile()();
                return value;
            }
            else
            {
                throw new NotSupportedException("MemberInfo type not supported: " + memberInfo.GetType().FullName);
            }
        }

        #endregion Helpers
    }
}
